package dungeon;

import java.util.Collection;
import java.util.ConcurrentModificationException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.NoSuchElementException;

import be.kuleuven.cs.som.annotate.Basic;
import be.kuleuven.cs.som.annotate.Model;
import be.kuleuven.cs.som.annotate.Raw;
import dungeonElements.Border;
import dungeonElements.Square;
import dungeonElements.TeleportationSquare;
import dungeonTools.Constraint;
import dungeonTools.Coordinate;
import dungeonTools.Direction;

/**
 * A class of Dungeons. Dungeons describe a subterranean space that can contain
 * multiple squares. They are mapped in 3d space, and each square has a certain 
 * coordinate here. Dungeons have a certain size, which can only grow while execution,
 * up to Long.MAX_VALUE. When squares are connected without any obstacles between them,
 * they form rooms, which all share the same temperature and level of humidity. These
 * rooms can also be retrieved as a whole from their dungeons.
 * 
 * The Dungeon class also offers a facility for retrieving which square is used in which
 * dungeon object.
 * 
 * @invar Each dungeon can have is current size as its size.
 * 		|canHaveAsSize(xSize(), getYSize(), getZSize())
 * @invar Squares in the same room are really in the same room
 * 		|(checkIfSquareIsInSameRoom(firstSquare,otherSquare)
 * 		|	== checkIfSquareIsInSameRoom(otherSquare, firstSquare))
 * @invar Dungeons can't have squares in positions with all the same coordinate values
 * 		|for(long i = 0; i < Long.MAX_VALUE; i++)
 * 		|	contains(new Coordinate(i,i,i) == false;
 * @invar Rooms remain coherent when unchanged
 * 		|HashSet<Square> oldRoom = getEnclosingRoom(square)
 * 		|RoomHandler.enforceRoomBorders(square)
 * 		|oldRoom.equals(new.getEnclosingRoom(square))
 * @invar Temperature and humidity values across rooms are equal
 * 		|Iterator<Square> iterator = getEnclosingRoom(square).iterator()
 * 		|while(iterator.hasNext(){
 * 		|	Square nextSquare = iterator.next()
 * 		|	nextSquare.getTemperatue().equals(square.getTemperature())
 * 		|		&& (nextSquare.getHumidity() == square.getHumidity)
 * @invar No more than 20% of the squares in a dungeon can have a slippery surface
 * 		|double nbOfSquares = 0;
 * 		|double nbOfSlipperySquares = 0;
 * 		|for( long x = 0, x < getXSize(); x++)
 * 		|	for( long y = 0, y < getYSize(); y++)
 * 		|		for( long z = 0, z < getZSize(); z++)
 * 		|		if(contains(new Coordinate(x,y,z)))
 * 		|			Square square = getSquareAtCoordinate(new Coordinate(x,y,z))
 * 		|			nbOfSquares++;
 * 		|			if(square.hasSlipperySurface())
 * 		|				nbOfSlipperySquares++;
 * 		|(nbOfSlipperySquares / nbOfSquares) <= 0.2
 * 
 * @author Tom Jacobs & Ewoud De Moor
 * @version 3.0
 */
public abstract class Dungeon<E extends Square> implements Iterable<E>{
	/**
	 * Static variable registering a mapping of each square onto the dungeon it's in
	 */
	private static final HashMap<Square,Dungeon<Square>> dungeonMap = new HashMap<Square, Dungeon<Square>>();
	
	/**
	 * Variable mapping each square onto the room it's in
	 */
	private final HashMap<E,HashSet<E>> roomMap = new HashMap<E, HashSet<E>>();
	
	/**
	 * Variable mapping each square onto the coordinate it's located at
	 */
	private final HashMap<E, Coordinate> squareMap = new HashMap<E,Coordinate>();
	
	/**
	 * Variable mapping each coordinate onto the square at that location
	 */
	private final HashMap<Coordinate, E> coordinateMap = new HashMap<Coordinate, E>();
	
	/**
	 * Variable mapping the size of this dungeon in the x-direction 
	 */
	private long xSize = 1L;
	
	/**
	 * Variable mapping the size of this dungeon in the y-direction 
	 */
	private long ySize = 1L;

	/**
	 * Variable mapping the size of this dungeon in the z-direction 
	 */
	private long zSize = 1L;
	
	/**
	 * A collection of all slippery squares
	 */
	private HashSet<E> slipperySquares = new HashSet<E>();

	/**
	 * The variable registering the amount of modifications done to this dungeon
	 */
	private long modCount;
	
	/**
	 * Checks whether or not a square is assigned to any dungeon
	 * 
	 * @param square
	 * 			The square to check
	 * @return	Whether or not the square is assigned to any dungeon
	 */
	@Basic
	public static <E> boolean isSquareInsideAnyDungeon(E square){
		return dungeonMap.containsKey(square);
	}
	
	/**
	 * A static method returning the dungeon object a particular square belongs to. 
	 * This method can be used to ensure squares aren't used in more than 1 dungeon.
	 * 
	 * @param square
	 * 			The squares whose enclosing dungeon we're interested in
	 * @return The dungeon containing this square
	 * 			|return.contains(square)
	 * @throws IllegalArgumentException
	 * 			The square must be initialized
	 * 			|(square == null) || (square.isTerminated())
	 * @throws IllegalArgumentException
	 * 			The square must be inside any dungeon
	 * 			|!Dungeon.isSquareInsideAnyDungeon(square)
	 */
	@SuppressWarnings("unchecked")
	public static <E extends Square> Dungeon<E> getDungeonContainingSquare(E square) throws IllegalArgumentException{
		if((square == null) || (square.isTerminated()))
			throw new IllegalArgumentException("Square not initialized");
		if(!dungeonMap.containsKey(square))
			throw new IllegalArgumentException("Square isn't inside any dungeon");
		return (Dungeon<E>) dungeonMap.get(square);
	}
	
	/**
	 * Finds the specific dungeon the square is in and checks the slippery state
	 * 
	 * @param square The square whose slipperySurface state has changed
	 * @throws IllegalStateException
	 * 			No more than 20% of the squares inside a dungeon can be slippery
	 */
	public static void squareChangedSlipperySurfaceState(Square square) throws IllegalStateException{
		Dungeon<Square> dungeon = Dungeon.getDungeonContainingSquare(square);
		dungeon.squareChangedSlipperySurface(square);
	}
	
	/**
	 * A method that is called to ensure consistency with the amount of Slippery Squares
	 * inside this dungeon.
	 * 
	 * @param square
	 * 			A square whose slippery state is being altered
	 * @throws IllegalStateException
	 * 			No more than 20% of the squares inside a dungeon can be slippery
	 * 			|!squareCanBeSlippery(square)
	 */
	@Raw
	protected void squareChangedSlipperySurface(E square) throws IllegalStateException{
		if(!squareCanBeSlippery(square))
			throw new IllegalStateException("Square can't be slippery, 20% threshold reached");
		if(square.hasSlipperySurface())
			slipperySquares.add(square);
		else{
			slipperySquares.remove(square);
		}
	}
	
	/**
	 * Determines whether or not a square can be inside this dungeon with 
	 * its current Slippery Surface State.
	 * 
	 * @param square
	 * 			The square to check for
	 * @return	Whether or not less than 20% of the squares in this dungeon are slippery,
	 * 			including square itself.
	 * 			|if(squareMap.size() == 0)
	 *			|	result == !square.hasSlipperySurface();
	 *			|if(square.hasSlipperySurface())
	 *			|	result == ((double)(slipperySquares.size() + 1) / (double)squareMap.size()) <= 0.2;
	 *			|result == ((double)(slipperySquares.size()) / (double)squareMap.size()) <= 0.2;
	 * @throws IllegalArgumentException
	 * 			The square must be initialized
	 * 			|(square == null) || (square.isTerminated())
	 */
	@Model @Raw
	protected boolean squareCanBeSlippery(E square) throws IllegalArgumentException{
		if((square == null) || (square.isTerminated()))
			throw new IllegalArgumentException("Square not initialized");
		if(squareMap.size() == 0)
			return !square.hasSlipperySurface();
		if(square.hasSlipperySurface())
			return ((double)(slipperySquares.size() + 1) / (double)squareMap.size()) <= 0.2;
		return ((double)(slipperySquares.size()) / (double)squareMap.size()) <= 0.2;
	}

	/**
	 * Returns the size this dungeon has in the x-direction
	 * 
	 * @return the size this dungeon has in the x-direction
	 */
	@Basic @Raw
	public long getXSize() {
		return xSize;
		
	}

	/**
	 * Sets the size of this dungeon in the x-direction to xSize.
	 * This size must be greater than the current size.
	 * 
	 * @param xSize
	 * 			The new size this dungeon will have in the x-direction
	 * @post This dungeon will have the given xSize as its xSize
	 * 			|new.getXSize() == xSize
	 * @effect If there is a parent dungeon, the new size will be set there too
	 * 			|if(hasParentDungeon())
	 * 			|	getParentDungeon().setNewDungeonSize(this);
	 * 			If this dungeon is the root node, set the new KDTreeSize
	 * 			|else if(this instanceof CompositeDungeon<?>)
	 * 			|	((CompositeDungeon<? extends Square>) this).setNewDungeonSize(this)
	 * @throws IllegalArgumentException
	 * 			The given zSize must be a valid size for this dungeon.
	 * 			|!canHaveAsNewSize(getXSize(), getYSize(), zSize)
	 */
	@Raw
	public void setXSize(long xSize) throws IllegalArgumentException{
		if(!canHaveAsSize(xSize, getYSize(), getZSize()))
			throw new IllegalArgumentException("This dungeon can't have the given xSize as xSize");
		this.xSize = xSize;
		if(hasParentDungeon())
			getParentDungeon().setNewDungeonSize(this);
		else if(this instanceof CompositeDungeon<?>)
			((CompositeDungeon<? extends Square>) this).setNewDungeonSize(this);
	}
	
	/**
	 * Returns the size this dungeon has in the y-direction
	 * 
	 * @return the size this dungeon has in the y-direction
	 */
	@Basic @Raw
	public long getYSize() {
		return ySize;
	}
	
	/**
	 * Sets the size of this dungeon in the y-direction to ySize.
	 * This size must be greater than the current size.
	 * 
	 * @param ySize
	 * 			The new size this dungeon will have in the y-direction
	 * @post This dungeon will have the given ySize as its ySize
	 * 			|new.getYSize() == ySize
	 * @effect If there is a parent dungeon, the new size will be set there too
	 * 			|if(hasParentDungeon())
	 * 			|	getParentDungeon().setNewDungeonSize(this);
	 * 			If this dungeon is the root node, set the new KDTreeSize
	 * 			|else if(this instanceof CompositeDungeon<?>)
	 * 			|	((CompositeDungeon<? extends Square>) this).setNewDungeonSize(this)
	 * @throws IllegalArgumentException
	 * 			The given ySize must be a valid size for this dungeon.
	 * 			|!canHaveAsNewSize(getXSize(), ySize, getZSize())
	 */
	@Raw
	public void setYSize(long ySize) throws IllegalArgumentException{
		if(!canHaveAsSize(getXSize(), ySize, getZSize()))
			throw new IllegalArgumentException("This dungeon can't have the given ySize as ySize");
		this.ySize = ySize;
		if(hasParentDungeon())
			getParentDungeon().setNewDungeonSize(this);
		else if(this instanceof CompositeDungeon<?>)
			((CompositeDungeon<? extends Square>) this).setNewDungeonSize(this);
	}

	/**
	 * Returns the size this dungeon has in the z-direction
	 * 
	 * @return the size this dungeon has in the z-direction
	 */
	@Basic @Raw
	public long getZSize() {
		return zSize;
	}

	/**
	 * Sets the size of this dungeon in the z-direction to zSize.
	 * This size must be greater than the current size.
	 * 
	 * @param zSize
	 * 			The new size this dungeon will have in the z-direction
	 * @post This dungeon will have the given zSize as its zSize
	 * 			|new.getZSize() == zSize
	 * @effect If there is a parent dungeon, the new size will be set there too
	 * 			|if(hasParentDungeon())
	 * 			|	getParentDungeon().setNewDungeonSize(this);
	 * 			If this dungeon is the root node, set the new KDTreeSize
	 * 			|else if(this instanceof CompositeDungeon<?>)
	 * 			|	((CompositeDungeon<? extends Square>) this).setNewDungeonSize(this)
	 * @throws IllegalArgumentException
	 * 			The given zSize must be a valid size for this dungeon.
	 * 			|!canHaveAsNewSize(getXSize(), getYSize(), zSize)
	 */
	@Raw
	public void setZSize(long zSize) throws IllegalArgumentException{
		if(!canHaveAsSize(getXSize(), getYSize(), zSize))
			throw new IllegalArgumentException("This dungeon can't have the given zSize as zSize");
		this.zSize = zSize;
		if(hasParentDungeon())
			getParentDungeon().setNewDungeonSize(this);
		else if(this instanceof CompositeDungeon<?>)
			((CompositeDungeon<? extends Square>) this).setNewDungeonSize(this);
	}

	/**
	 * Check if this dungeon can have the given size as its size.
	 *
	 * @param xSize
	 * 		The xSize to check.
	 * @param ySize
	 * 		The ySize to check.
	 * @param zSize
	 * 		The zSize to check.
	 * @return The new size must be greater than or equal to the current size.
	 * 		| if(xSize < getXSize()) 
	 * 		| 	then result == false
	 * 		| if(ySize < getYSize())
	 * 		| 	then result == false
	 * 		| if(zSize < getZSize())
	 * 		| 	then result == false
	 * @return If there is a parent dungeon, consistency must be checked
	 *		| if(hasParentDungeon())
	 *		|	then result == getParentDungeon().childDungeonCanHaveNewSize(this, xSize, ySize, zSize)
	 * @return In all other cases, true is returned
	 * 		|	result == true
	 */
	@Raw
	public boolean canHaveAsSize(long xSize, long ySize, long zSize){
		if(xSize < getXSize()) 
			return false;
		if(ySize < getYSize())
			return false;
		if(zSize < getZSize())
			return false;
		if(hasParentDungeon())
			return getParentDungeon().childDungeonCanHaveNewSize(this, xSize, ySize, zSize);
		return true;
	}
	
	
	/**
	 * Checks whether or not 2 squares share the same room.
	 * This method runs with complexity O(1), or constant time.
	 * 
	 * @param firstSquare
	 * 			The square to check against
	 * @param otherSquare
	 * 			The square to be checked
	 * @return	Whether or not the squares share a room
	 * 			|getEnclosingRoom(firstSquare).contains(otherSquare)
	 */
	public boolean isSquareInSameRoom(E firstSquare, E otherSquare){
		return getEnclosingRoom(firstSquare).contains(otherSquare);
	}
	
	/**
	 * Checks whether or not a certain other square is reachable starting
	 * from a square in this dungeon
	 * 
	 * @param squareInThisDungeon
	 * 			The square situated in this dungeon
	 * @param otherSquare
	 * 			The square we want to check the reachability of
	 * @return	Whether or not this other square is reachable
	 * 			|result == getAllReachableSquares(squareInThisDungeon).contains(otherSquare);
	 * @throws IllegalArgumentException
	 * 			The square must be inside this dungeon
	 * 			|!contains(squareInThisDungeon)
	 */
	public boolean isSquareReachable(E squareInThisDungeon, E otherSquare) throws IllegalArgumentException{
		if(!contains(squareInThisDungeon))
			throw new IllegalArgumentException("The square isn't inside this dungeon");
		return getAllReachableSquares(squareInThisDungeon).contains(otherSquare);
	}
	
	/**
	 * Returns a collection of all squares that can be (theoretically) reached starting
	 * from the single square as parameter.
	 * 
	 * @param square
	 * 			The square whose reachable area will be returned
	 * @return	The collection of all reachable squares
	 * 			|let
	 * 			|	HashSet<E> squaresToAdd = getEnclosingRoom(square);
	 * 			|	HashSet<E> allReachableSquares = new HashSet<E>();
	 * 			|in
	 * 			|	while(squaresToAdd.size() > 0){
	 * 			|	let
	 * 			|		HashSet<E> targetSquares = new HashSet<E>();
	 * 			|	in
	 * 			|		for(E teleportationSquare : squaresToAdd)
	 * 			|			targetSquares.addAll((Collection<? extends E>) ((TeleportationSquare) teleportationSquare).getAllTeleportationDestinations());
	 * 			|		allReachableSquares.addAll(squaresToAdd);
	 * 			|		squaresToAdd.clear();
	 * 			|		for(E targetSquare : targetSquares){
	 * 			|			if 
	 * 			|				Dungeon.isSquareInsideAnyDungeon(targetSquare)
	 * 			|			then let
	 * 			|				Dungeon<E> targetDungeon = (Dungeon<E>) Dungeon.getDungeonContainingSquare(targetSquare);
	 * 			|				if
	 * 			|					!isSquareInSameRoom(square, targetSquare)
	 * 			|				then
	 * 			|					squaresToAdd.addAll((Collection<? extends E>) targetDungeon.getEnclosingRoom(targetSquare));
	 * 			|		}
	 * 			|result == allReachableSquares
	 * @throws IllegalArgumentException
	 * 			The square must be inside this dungeon
	 * 			|!contains(square)
	 */
	@SuppressWarnings("unchecked")
	public HashSet<E> getAllReachableSquares(E square) throws IllegalArgumentException{
		if(!contains(square))
			throw new IllegalArgumentException("The square isn't inside this dungeon");
		HashSet<E> squaresToAdd = getEnclosingRoom(square);
		HashSet<E> allReachableSquares = new HashSet<E>();
		while(squaresToAdd.size() > 0){
			HashSet<E> targetSquares = new HashSet<E>();
			for(E teleportationSquare : squaresToAdd)
				targetSquares.addAll((Collection<? extends E>) ((TeleportationSquare) teleportationSquare).getAllTeleportationDestinations());
			allReachableSquares.addAll(squaresToAdd);
			squaresToAdd.clear();
			for(E targetSquare : targetSquares){
				if(Dungeon.isSquareInsideAnyDungeon(targetSquare)){
					Dungeon<E> targetDungeon = (Dungeon<E>) Dungeon.getDungeonContainingSquare(targetSquare);
					if(!isSquareInSameRoom(square, targetSquare))
						squaresToAdd.addAll((Collection<? extends E>) targetDungeon.getEnclosingRoom(targetSquare));
				}
				else
					squaresToAdd.add(targetSquare);
			}
		}
		return allReachableSquares;
	}

	/**
	 * Returns a collection of squares describing the room this square is in,
	 * including itself.
	 * This method runs with complexity O(1), or constant time.
	 * 
	 * @param square
	 * 			The square whose room must be returned
	 * @return	The collection of squares in the room enclosing this square
	 * @throws IllegalArgumentException
	 * 			The square must be inside this dungeon
	 * 			|!contains(square)
	 */
	@Raw
	public HashSet<E> getEnclosingRoom(E square) throws IllegalArgumentException{
		if(!contains(square))
			throw new IllegalArgumentException("The square isn't inside this dungeon");
		return new HashSet<E>(roomMap.get(square));
	}

	/**
	 * Returns the square at a certain coordinate inside this dungeon
	 * This method runs with complexity O(1), or constant time.
	 * 
	 * @param coordinate
	 * 			The coordinate of the square that will be returned
	 * @return	The square at that coordinate
	 * @throws IllegalArgumentException
	 * 			The coordinate must be assigned in this dungeon
	 * 			|!contains(coordinate)
	 */
	@Basic
	public Square getSquareAtCoordinate(Coordinate coordinate) throws IllegalArgumentException{
		if(!contains(coordinate))
			throw new IllegalArgumentException("Coordinate not assigned in this dungeon");
		return coordinateMap.get(coordinate);
	}

	/**
	 * Returns the coordinate of a certain square inside this dungeon
	 * This method runs with complexity O(1), or constant time.
	 * 
	 * @param square
	 * 			The square of which the coordinate will be returned
	 * @return	The coordinate of that square
	 * @throws IllegalArgumentException
	 * 			The square must be assigned in this dungeon
	 * 			|!contains(square)
	 */
	public Coordinate getCoordinateOfSquare(E square) throws IllegalArgumentException{
		if(!contains(square))
			throw new IllegalArgumentException("Square not assigned in this dungeon");
		return squareMap.get(square);
	}

	/**
	 * Returns whether or not the square is inside this dungeon
	 * This method runs with complexity O(1), or constant time.
	 *  
	 * @param square
	 * 			The square that will be checked
	 * @return	Whether or not this dungeon contains the square
	 * @throws IllegalArgumentException
	 * 			The square has to be initialized
	 * 			|(square == null) || (square.isTerminated())
	 */
	@Raw
	public boolean contains(E square) throws IllegalArgumentException{
		if((square == null) || (square.isTerminated()))
			throw new IllegalArgumentException("Square not initialized");
		return roomMap.containsKey(square) || squareMap.containsKey(square);
	}

	/**
	 * Returns whether or not the coordinate is occupied by a square in this dungeon.
	 * This method runs with complexity O(1), or constant time.
	 * 
	 * @param coordinate
	 * 			The coordinate at which will be checked
	 * @return	Whether or not that coordinate is occupied by a square
	 * @throws IllegalArgumentException
	 * 			The coordinate must be initialized
	 * 			|coordinate == null
	 */
	@Raw
	public boolean contains(Coordinate coordinate) throws IllegalArgumentException{
		if(coordinate == null)
			throw new IllegalArgumentException("Coordinate not initialized");
		return coordinateMap.containsKey(coordinate);
	}


	/**
	 * Checks whether or not a square can be placed at a specific coordinate
	 * 
	 * @param square
	 * 			The square to place
	 * @param coordinate
	 * 			The coordinate at which it should be placed
	 * @return 	The parameters must be initialized
	 * 			|if((square == null) || (square.isTerminated()) || (coordinate == null))
	 * 			|	then result == false
	 * 			False if the square is already used in a dungeon
	 * 			|if(isSquareInsideAnyDungeon(square))
	 * 			|	then result == false
	 * 			False if the coordinate is already in use
	 * 			|if(this.contains(coordinate)
	 * 			|	then result == false
	 * 			Coordinates are not allowed to be of the format (x,x,x),
	 * 			meaning they have 3 equal ordinates.
	 * 			|if((coordinate.getxCoordinate() == coordinate.getyCoordinate()) 
	 *			|	&& coordinate.getxCoordinate() == coordinate.getzCoordinate())
	 *			|	then result == false
	 *			False if the square causes the 20% slippery-surfaces-threshold to be exceeded
	 * 			True in all other cases
	 * 			|else result == true
	 */
	public boolean canHaveSquareAtCoordinate(E square, Coordinate coordinate){
		if((square == null) || (square.isTerminated()) || (coordinate == null))
			return false;
		if(isSquareInsideAnyDungeon(square))
			return false;
		if(contains(coordinate))
			return false;
		if((coordinate.getxCoordinate() == coordinate.getyCoordinate()) 
				&& coordinate.getxCoordinate() == coordinate.getzCoordinate())
			return false;
		if(!squareCanBeSlippery(square) && square.hasSlipperySurface())
			return false;
		if((coordinate.getxCoordinate() >= getXSize()) 
				|| (coordinate.getyCoordinate() >= getYSize())
				|| (coordinate.getzCoordinate() >= getZSize()))
			return false;
		return true;
	}
	
	/**
	 * Adds a square to the dungeon. Will also instantly enforce possible
	 * new rooms, room borders and internal room data.
	 * 
	 * @param square
	 * 			The square to be added
	 * @param coordinate
	 * 			The coordinate at which the square should be added
	 * @post 	The square is located at that coordinate
	 * 			|new.getSquareAtCoordinate(coordinate).equals(square)
	 * 			|	&& new.getCoordinateOfSquare(square).equals(coordinate)
	 * @post	The square is used inside this dungeon
	 * 			|Dungeon.getDungeonContainingSquare(square).equals(new)
	 * @effect 	Enforces the room's borders
	 * 			|RoomHandler.enforceRoomBorders(square)
	 * @throws IllegalArgumentException
	 * 			The dungeon must be able to have the square at that coordinate
	 * 			|!canHaveSquareAtCoordinate(square, coordinate)
	 */
	@SuppressWarnings("unchecked")
	@Raw
	public void addSquare(E square, Coordinate coordinate) throws IllegalArgumentException{
		if(!canHaveSquareAtCoordinate(square, coordinate))
			throw new IllegalArgumentException("Can't have that square at that coordinate");
		modCount++;
		dungeonMap.put(square, (Dungeon<Square>) this);
		squareMap.put(square, coordinate);
		coordinateMap.put(coordinate, square);
		for(Direction direction : Direction.values()){
			Square nextSquare = RoomHandler.getNextSquareInDirection(square, direction);
			if(nextSquare != null && (!nextSquare.isTerminated())){
				Border border = nextSquare.getBorder(direction.getOppositeDirection());
				if(square.isBorderEqualOrUpgrade(border, direction)){
					square.upgradeBorderTo(border, direction);
				}
				else{
					nextSquare.upgradeBorderTo(square.getBorder(direction), direction.getOppositeDirection());
				}
			}
		}
		if(square instanceof TeleportationSquare)
		RoomHandler.enforceRoomBorders((TeleportationSquare) square);
	}

	/**
	 * Removes a square from the game
	 * 
	 * @param square
	 * 			The square to remove
	 * @effect Searches the square to remove and does so
	 * 			|Dungeon dungeon = Dungeon.getDungeonContainingSquare(square);
	 * 			|dungeon.remove(dungeon.getCoordinateOfSquare(square));
	 */
	@Raw
	public static void remove(Square square){
		Dungeon.getDungeonContainingSquare(square)
			.remove(Dungeon.getDungeonContainingSquare(square).getCoordinateOfSquare(square));
	}

	/**
	 * Removes the square at a specific coordinate from the game
	 * 
	 * @param coordinate
	 * 			The coordinate a which to remove the square
	 * @effect 	Terminates the square
	 * 			|Square square = getSquareAtCoordinate(coordinate)
	 * 			|square.terminate()
	 * @post	The square and coordinate aren't in this dungeon anymore
	 * 			|new.contains(coordinate) == false
	 * 			|new.contains(square) == false
	 * @post	The square isn't inside any dungeon anymore
	 * 			|!Dungeon.isSquareInsideAnyDungeon(square)
	 * @throws IllegalArgumentException
	 * 			The coordinate must be in use
	 * 			|!contains(coordinate)
	 */
	@Raw
	public void remove(Coordinate coordinate) throws IllegalArgumentException{
		if(!contains(coordinate))
			throw new IllegalArgumentException("Coordinate not in use in this dungeon");
		modCount++;
		Square square = getSquareAtCoordinate(coordinate);
		dungeonMap.remove(square);
		squareMap.remove(square);
		if(roomMap.containsKey(square))
			roomMap.get(square).remove(square);
		roomMap.remove(square);
		coordinateMap.remove(coordinate);
		square.terminate();
	}
	
	/**
	 * Returns a string containing a description of all squares that make up
	 * the room around a certain square
	 * 
	 * @param square
	 * 			The square whose room to describe
	 * @return	A string containing the descriptions
	 * 			|String roomDescription = "";
	 * 			|Iterator<Square> roomIterator = getEnclosingRoom(square).iterator();
	 * 			|while(roomIterator.hasNext()){
	 * 			|	roomDescription += "Square at: " + getCoordinateOfSquare(roomIterator.next()) + "\n";
	 * 			|}
	 * 			|String trimmedRoomDescription = roomDescription.substring(0, roomDescription.length()-1);
	 * 			|result == trimmedRoomDescription;
	 * @throws IllegalArgumentException
	 * 			The square must be in use
	 * 			|!contains(square)
	 */
	public String getRoomDescription(E square) throws IllegalArgumentException{
		if(!contains(square))
			throw new IllegalArgumentException("Square not in use in this dungeon");
		String roomDescription = "Room description:\n";
		Iterator<E> roomIterator = getEnclosingRoom(square).iterator();
		while(roomIterator.hasNext()){
			E nextSquare = roomIterator.next();
			String className = ""+ nextSquare.getClass();
			className = className.substring(className.lastIndexOf(".")+1);
			roomDescription += className +" at: " + getCoordinateOfSquare(nextSquare) + "\n";
		}
		String trimmedRoomDescription = roomDescription.substring(0, roomDescription.length()-1);
		return trimmedRoomDescription;
	}
	
	
	/**
	 * A package-private method allowing the RoomHandler to set entire rooms at once
	 * 
	 * @param square
	 * 			The square for who the room will be set
	 * @param room
	 * 			The room that will be set. 
	 * @post	All squares inside this room will map onto this new room.
	 * 			|Iterator<Square> iterator = room.iterator();
	 * 			|while(iterator.hasNext()){
	 * 			|	new.getEnclosingRoom(iterator.next()).equals(room);
	 * @throws IllegalArgumentException
	 * 			The parameters must be valid
	 * 			|(room == null) || (square == null) || !room.contains(square) || square.isTerminated()
	 */
	@Raw
	protected void setRoom(E square, HashSet<E> room) throws IllegalArgumentException{
		if((room == null) || (square == null) || !room.contains(square) || square.isTerminated())
			throw new IllegalArgumentException("Please initialize the parameters");
		roomMap.put(square, room);
	}
	
	/**
	 * Sets the parent dungeon of this dungeon the parameter
	 * 
	 * @param compositeDungeon
	 * 			The dungeon that will be set as parent of this dungeon
	 * @post	The dungeon will have that dungeon as parent
	 * 			|new.getParentDungeon() == compositeDungeon
	 * @throws IllegalArgumentException
	 * 			The dungeon must have this one as its child
	 * 			|!compositeDungeon.hasAsChildDungeon(this)
	 * @throws IllegalStateException
	 * 			|This dungeon can't already have a parent
	 * 			|hasParentDungeon()
	 */
	public void setParentDungeon(CompositeDungeon<? extends Square> compositeDungeon) throws IllegalArgumentException,IllegalStateException{
		if(hasParentDungeon())
			throw new IllegalStateException("This dungeon already has a parent dungeon");
		if(!compositeDungeon.hasAsChildDungeon(this))
			throw new IllegalArgumentException("The parent dungeon doesn't have this one as its child");
		this.parentDungeon = compositeDungeon;
	}

	/**
	 * Gets the parent dungeon of this dungeon
	 * 
	 * @return	The parent dungeon of this dungeon
	 * @throws IllegalArgumentException
	 * 			This dungeon must have a parent dungeon
	 * 			|!hasParentDungeon()
	 */
	@Basic
	public CompositeDungeon<? extends Square> getParentDungeon() throws IllegalArgumentException{
		if(!hasParentDungeon())
			throw new IllegalStateException("This dungeon is the root dungeon");
		return parentDungeon;
	}
	
	/**
	 * Gets the root dungeon containing this dungeon.
	 * 
	 * @return The root dungeon
	 * 			|let Dungeon<E> parentDungeon = this
	 * 			|while(parentDungeon.hasParentDungeon()){
	 * 			|	parentDungeon = (Dungeon<E>) parentDungeon.getParentDungeon()
	 * 			|}
	 * 			|result == parentDungeon
	 */
	@SuppressWarnings("unchecked")
	public Dungeon<E> getRootDungeon(){
		Dungeon<E> parentDungeon = this;
		while(parentDungeon.hasParentDungeon()){
			parentDungeon = (Dungeon<E>) parentDungeon.getParentDungeon();
		}
		return parentDungeon;
	}
	
	/**
	 * Returns whether or not this dungeon has a parent dungeon
	 * 
	 * @return whether or not this dungeon has a parent dungeon
	 */
	public boolean hasParentDungeon() {
		return parentDungeon != null;
	}
	
	private CompositeDungeon<? extends Square> parentDungeon;
	
	/**
	 * Returns an iterator over this dungeon. It will iterate over every
	 * single square inside this dungeon. The Iterator will be parameterized
	 * with the same type of this dungeon, all extending Square.
	 * 
	 * The returned iterator also has an additional method for returning
	 * all square matching a certain constraint.
	 * 
	 * @return A new Iterator over all squares in this dungeon
	 */
	@Override
	public Iterator<E> iterator() {
		return this.new DungeonIterator(this);
	}
	
	/**
	 * An Iterator that iterates over all squares inside a Dungeon.
	 * Also features the getAllSquaresMatchingConstraint(constraint) method that
	 * returns all squares inside the dungeon matching a certain constraint.
	 * 
	 * @author Tom Jacobs & Ewoud De Moor
	 * @version 1.0
	 */
	public class DungeonIterator implements Iterator<E>{

		private long modificationCount = modCount;
		private Iterator<E> it = roomMap.keySet().iterator();
		/* 
		 * READ THIS PLEASE:
		 * 
		 * Choosing to use the built in iterator function of Sets in Java is equivalent
		 * with using reflection to loop through the private 'elementData' array in
		 * HashMap's implementation, but a lot less open for errors, and independent 
		 * of future changes to the private (static) class AbstractMapIterator<K, V>.
		 * 
		 */
		private E currentSquare;
		private Dungeon<E> dungeon;
		
		
		private DungeonIterator(Dungeon<E> dungeon) {
			this.dungeon = dungeon;
		}
		@Override
		public boolean hasNext() {
			concurrentModificationCheck();
			return it.hasNext();
		}

		private void concurrentModificationCheck() {
			if(modCount != modificationCount)
				throw new ConcurrentModificationException("Please only use the iterator to modify this dungeon");
			
		}
		@Override
		public E next() throws NoSuchElementException{
			if(!hasNext())
				throw new NoSuchElementException("No more elements found");
			currentSquare = it.next();
			return currentSquare;
		}

		@Override
		public void remove() {
			concurrentModificationCheck();
			if(currentSquare == null)
				throw new IllegalStateException("There is no element to remove");
			modificationCount++;
			if(roomMap.containsKey(currentSquare))
				dungeon.roomMap.get(currentSquare).remove(currentSquare);
			it.remove();
			dungeon.remove(getCoordinateOfSquare(currentSquare));
			currentSquare = null;
		}
		
		/**
		 * Returns all squares in this dungeon that match the constraint
		 * 
		 * @param constraint
		 * 			The constraint to match
		 * @return 	All squares matching that constraint
		 */
		public HashSet<E> getAllSquaresMatchingConstraint(Constraint constraint){
			HashSet<E> allSquaresMatchingConstraint = new HashSet<E>();
			for(E square : squareMap.keySet()){
				if(constraint.matchesConstraint(square))
					allSquaresMatchingConstraint.add(square);
			}
			return allSquaresMatchingConstraint;
		}
	}
}
